上一章我们对离线存储进行了讲解,本节我们继续介绍离线处理中的另外一个话题 - 后台同步,该机制允许用户随时随地进行事务处理,而无需关心网络的连接状态。比如以下示例:

通过演示我们可以看到,无论在线还是离线,甚至在触发了后台同步之后关闭页面,只要网络处于在线状态都会执行后台同步事件,并将缓存在本地的请求数据发送到服务端。对于传统的 Web 应用来说,该特性是极其令人兴奋的,因为它解决了传统 Web 应用所存在的以下几个问题:
- 页面发起的请求会随着页面的关闭而终止。
- 在离线状态下,很难将用户的网络请求缓存起来,并在网络恢复正常后再次进行请求。
那么它究竟是如何工作的?下面我们通过上面的演示实例对其进行说明。
# 基本使用
# 注册
在上面的演示中,当我们点击添加按钮后,控制台首先输出了已触发后台同步:add-todo,这便完成了后台同步事件注册,主要代码如下:
<script src="./db.js"></script>
<script src="./ui.js"></script>
<script src="./network.js"></script>
<script>
window.addEventListener('load', function() {
if ('serviceWorker' in navigator && 'SyncManager' in window) {
navigator.serviceWorker.register('./sw.js').then(function(registration) {
document.getElementById('submit').addEventListener('click', function() {
ui.submit(function(name) {
db.addTodo(name).then(function() {
registration.sync.register('add-todo').then(function() {
console.log('已触发后台同步:add-todo');
});
});
});
});
});
navigator.serviceWorker.addEventListener('message', function(event) {
ui.render(event.data);
});
} else {
document.getElementById('submit').addEventListener('click', function() {
ui.submit(function(name) {
network.addTodos([{ name: name }]).then(function(todos) {
ui.render(todos);
});
});
});
}
});
</script>
上述代码中,我们首先判断当前环境下
Service Worker及SyncManager是否可用,如不可用则按照传统方式对按钮的点击事件进行处理,否则按照以下步骤进行处理:
- 注册
Service Worker。 - 注册完成后,使用回调参数
registration的sync.register方法注册一个后台同步事件;在该例中,我们在回调中监听按钮的点击事件,并在点击事件中进行后台同步事件注册。 - 当添加按钮点击且页面验证通过后(通过
ui.submit方法),我们通过db.addTodo方法将需要发送到服务端的数据缓存在本地。 - 数据缓存成功后,则调用
registration.sync.register方法注册一个名为add-todo的后台同步事件。
以上便是后台同步注册的常规流程,其过程非常简单,但也需要注意以下几点:
registration.sync.register的参数是事件的唯一标识,为了减少设备、浏览器需要唤醒的次数,浏览器可能会将多个具有相同标识的事件合并为一个;如果想要每个事件都触发一次,则需要使用不同的标识。- 由于
Service Worker内不允许直接访问DOM元素,因此我们需要将发送到服务端的数据缓存到本地(根据上一章的讨论,我们一般使用IndexedDB进行处理)。
# 响应
完成了注册,接下来我们就需要在
Service Worker中对事件进行响应,主要代码如下:
importScripts('./db.js');
importScripts('./network.js');
function notification(todos) {
self.clients.matchAll().then(function(clients) {
if (clients && clients.length) {
clients.forEach(function(client) {
client.postMessage(todos);
});
}
});
}
self.addEventListener('sync', function(event) {
if (event.tag === 'add-todo') {
console.log(`开始进行后台同步:${event.tag}`);
event.waitUntil(
db.getTodos().then(function(todos) {
return network.addTodos(todos).then(function(todos) {
console.log('来自服务器的响应:', todos);
notification(todos);
return db.clearTodos();
});
})
);
}
});
因为
Service Worker是独立于主线程运行的,所以即使在页面中引入了./db.js及./network.js,我们仍需要通过importScripts方法将其引入。 而后,我们通过监听sync事件来响应同步事件,在回调函数中,我们主要做了以下事情:
- 检查当前同步的标签(这里为
add-todo),从而触发正确的同步逻辑。 - 获取存储在本地的需要发送到服务端的数据,并将其发送到服务端。
- 同步完成后通知主线程并删除本地缓存。
至此我们完成了后台同步事件的响应,过程依旧非常简单,这里需要注意以下事项:
sync回调函数返回值为Promise对象,由于浏览器内置了智能的重试机制,所以我们无需自行设计重试机制。- 上文已提到过,
Service Worker是无法直接操作DOM元素的,因此如果我们在同步处理成功后想要对DOM进行处理,可通过给主线程发送消息来实现:
// Service Worker 内实现参见 notification 方法
// 主线程
navigator.serviceWorker.addEventListener('message', function(event) {
// doSomething with event.data;
});
# 重试
上文中我们提到,针对
sync事件中的异常,浏览器内置了智能的重试机制,但它究竟何时执行且是否会一直执行下去呢?下面我们通过实际测试来回答这个问题。

通过上图的输出,我们可以看出:
- 第一次执行失败后,第二次会在 5 分钟之后触发;
- 第二次执行失败后,第三次会在 15 分钟后触发;
- 如果第三次执行失败后,该同步事件将不会再触发。
总的来说,从我们通过 registration.sync.register 注册一个同步事件开始,到该事件的落幕,这期间它最多可被执行 3 次。如果想要在它惨淡落幕前给予用户提示,可通过以下方式实现:
self.addEventListener('sync', function(event) {
if (event.tag === 'add-todo') {
event.waitUntil(
doSomething().catch(function(error) {
if (event.lastChance) {
//给予用户以友好提示
}
throw error;
})
);
}
});
# 总结
本章中,我们详细介绍了后台同步,它为实现恶劣网络环境下,用户进行无感知的事务处理提供了可能。至此我们完成了 Service Worker、离线存储、后台同步的学习,通过这些技术我们可以构建出应对复杂网络状况的 Web 应用,以此提高用户体验并逐步抹平与原生应用在离线处理方面的差异。在接下来的一个章节中,我们将对推送通知进行讨论,通过它我们可以让 Web 应用彻底突破浏览器限制,以实现曾经可望而不可及的系统深度集成
← 基础篇 3 离线存储 基础篇 5 推送通知 →